Skip to content

feat(objectql): declare a legitimately org-less write instead of inferring it from NULL - #14923

Closed
os-musk wants to merge 10 commits into
mainfrom
claude/issue-13636-orgless-write-declaration
Closed

feat(objectql): declare a legitimately org-less write instead of inferring it from NULL#14923
os-musk wants to merge 10 commits into
mainfrom
claude/issue-13636-orgless-write-declaration

Conversation

@os-musk

@os-musk os-musk commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Fixes #13636

Implements direction B of the maintainer's 2026-08-31 ruling (总监席第 7 场决裁批 #17): the platform gets an explicit per-write "legitimately org-less" declaration, and resolveSystemInsertOrganization uses it to tell 「有意的环境级/无租户行」 from 「漏 stamp 的 bug」.

Verified at f5d20ac01, merged up to origin/main.

This PR is deliberately UNARMED and must stay that way for now. An isolated contract review found that an audit write refused by ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED is swallowed and lost by plugin-audit's best-effort failure path — including the audit row for an env-level sys_metadata write declared exactly as this PR declares it. That finding is open, it is a p1 security-path question in another lane, and this seat has put it to the maintainer rather than working around it. ⛔ Do not narrow the admission to dodge it: the ruling's constraint 3 admits both specimens.

Constraint 3 first — the class-size census, before the channel

Re-derived at f5d20ac01 with the engine's OWN reach rule, resolveTenantFieldName (tenancy/system-write-organization.ts), applied to the registered schema — that is, after applySystemFields / resolveInjectedSystemColumns, which is what the engine and the driver actually see. ⚠️ That resolver does not read managedBy, and an earlier revision of this body counted as though it did; the figures below replace it.

reading on origin/main after this PR
platform-namespace objects registered in packages/ 84 84
…outside the machinery (resolveTenantFieldName answers null) 25 25
…carrying a tenant column, i.e. in the machinery's reach 59 59
of those 59 — tenant-scoped 8 8
of those 59 — global 1 1
of those 59 — conditional 0 2
of those 59 — unclassified 50 48

The ledger header at platform-object-tenancy.ts:32-37 has NOT drifted. Its 84 / 25 / 59 reproduce exactly under an independent re-measurement, and the earlier claim in this body that it had drifted to 28 / 56 was wrong — it came from treating managedBy: 'better-auth' as "no tenant field", which the resolver never does. sys_member, sys_team and sys_invitation are better-auth managed and declare organization_id: Field.lookup(...) with no tenancy block (sys-member.object.ts:20,232; sys-team.object.ts:130; sys-invitation.object.ts:200), so all three resolve organization_id and are in reach.

The writer-site split is re-derived against the pool the choice was actually made from — the 50 objects that were unclassified on origin/main. Predicate, stated so it is checkable: some tracked non-test .ts under packages/ contains a call to one of the three engine write doors naming the object as a literal first argument (insert('name', update('name', delete('name').

reading count
unclassified objects on origin/main 50
…with any in-repo engine write-door site naming them literally 19
…with none (their writers are not in this tree) 31
conditionally tenant-scoped with a citable writer fact 2

The class is the two ruled specimens. No unclassified object carries the citable both-populations writer fact the admission bar demands, so nothing was picked by guess. The ruling's ⛔ 不从 #13491 的 cannot-determine 里凭猜挑成员 is satisfied by measurement rather than by restraint, and conditionalPlatformObjects() is pinned as a list (['sys_audit_log','sys_metadata']), not a count.

⚠️ NOT MEASURED — declared, not quietly scoped away

cloud and objectos are outside this session's repo scope, and they are where host/environment-level write paths live. This census says nothing about them. The 31 in-repo objects with no local writer are the visible edge of that gap: their writers are somewhere this tree cannot read. A member admitted from either repo needs its own census run there.

Zone 2 assumptions — all four measured, all four hold

# assumption verdict
A the post-hook declared-field door runs before resolveSystemInsertOrganization, both reading rowHookContexts holds, line-for-line
B PLATFORM_PROVISIONED_COLUMNS is exactly ['id','created_at','updated_at'] with a landed ⛔ against a per-key exemption list holds
C the resolver receives the ExecutionContext, which the door never inspects holds — but not the route taken, see below
D the classification vocabulary lives in tenancy/platform-object-tenancy.ts + tenancy/system-write-organization.ts holds

So a declaration carried as a key on the row payload is refused before the resolver it informs ever sees it. Confirmed, and it is why the declaration rides the write options.

Why not the context route (assumption C)

Engine.insert's option type is an intersection, so objectql contributes its own member without editing ExecutionContext or DataEngineInsertOptions, and the caller's options object survives by reference into rowHookContexts[i].input.options, which the resolver's call site reads. insert() already carries a precedent for an objectql-owned option key (__partialRowErrors).

A context-borne declaration would also have been wrong on the merits: ScopedContext.sudo() copies the context across every write it makes, so one object's declaration would ride onto another's row.

⚠️ One packages/spec change WAS required, and an earlier revision of this body denied it

OrgLessWriteDeclarationRefusedError declares status = 500, and the REST layer forwards a string code on any status >= 500 (error-response.ts:1082-1092). error-code-ledger.zod.ts:10-16 rules that every service-specific code a route may put in error.code is registered there, and an unregistered code fails schema parse. So ERR_ORGLESS_WRITE_DECLARATION_REFUSED is wire vocabulary whether or not a route means it to be, and it is now registered under @objectstack/objectql, beside its exact sibling ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED. Precedent for a refusal's code shipping in the refusal's own PR: FILE_FIELD_BULK_WRITE_REFUSED at error-code-ledger.zod.ts:377, added by a5302c7a3 (PR #7224).

The diff into that single-owner surface is one ledger entry plus the two generated rows it produces (content/docs/references/api/contract.mdx, .../error-code-ledger.mdx — 3 lines, regenerated by check:generated --fix), and nothing else.

⚠️ Honest caveat, carried rather than buried: no gate reds on this omission, and three registry-time codes in the tree are also unledgered. Those three are not status-bearing wire codes, so they are not precedent for leaving this one out — but the absence of a gate means the next author is in the same position, and only a reader catches it.

The shape

  • A fourth ledger verdict, conditional — both populations, split per ROW. It ADMITS an object into System-context writes land untenanted at RUNTIME, so a single-tenant install keeps re-forking the autonumber scope and minting duplicate business identifiers — the producer #8686's backfill cannot reach (17.0.0 GA) #8844's derive-or-refuse machinery, so it is the strictest verdict, not a softer one: after this PR an undeclared org-less system write on sys_metadata / sys_audit_log is derived on a single-organization install and refused loudly on a walled one, exactly as a tenant-scoped object's is.
  • The declarationorgLessWrite: { object, reason } on the write options, with a closed reason vocabulary. It names its own object, which is what stops it travelling.
  • Every unadmitted spelling THROWS (ERR_ORGLESS_WRITE_DECLARATION_REFUSED, 500) — wrong object, unadmitted reason, non-conditional object, malformed value, and (since this round) an unadmitted declaration on an empty batch, which was the one spelling that used to be silent. The check runs ahead of every early return in the resolver, deliberately: a check one line lower would silently ignore a declaration on any early-return object, and an option with a silently-ignored spelling is the 「静默可选标记」 the ruling disqualified by name. That placement is pinned by its own tests.

loud · checkable · countable

word discharged by
loud spelled at the call site next to the row it describes; every misuse throws at the write
checkable validated against PLATFORM_OBJECT_TENANCY, whose conditional entries each cite a writer fact
countable new gate pnpm check:orgless-write-declarations enumerates every declaration in the tree, holds each to the same ledger, and prints the count: 6 declaration(s) across 6 file(s) — sys_audit_log=5, sys_metadata=1; 2 object(s) classified 'conditional'

⚠️ The countable half was itself uncountable when this PR was first pushed, and that is worth stating rather than fixing quietly. The gate's file filter spelled the repo tooling root as a quoted literal, and extractWatchHints (scripts/pm/dispatch-gates.mjs) reads any quoted span carrying a separator as the gate DECLARING that population — while the literal's whole job is to EXCLUDE that tree. The false declaration collapsed to a bare top-level word hintCovers refuses as too generic, so the gate scored silent for every card in the tree: a count that exists and that no dispatch derivation can ever name. check:pm-dispatch-gates reds on exactly that, and it did. Repaired by remedy (b) — stop spelling a bare root, respell the literal to say what the predicate means, as an anchored regex, the worked instance being check-published-files.mjs. ⛔ Remedy (a), the ROOT_DIR_WATCH_HINTS subtree declaration, would have been a false declaration here (it would name the gate for every repo-root tooling edit it does not read) and was refused for that reason; ⛔ nothing was added to the shrink-only escapable-literal ledger — the row discharged by construction. The gate's own docblock now carries both refusals so the next author does not reach for either.

The declaration is a plain literal key rather than a factory call because @objectstack/metadata-protocol writes sys_metadata and cannot import from @objectstack/objectql — objectql depends on it, so the edge would be a cycle. One spelling is writable and countable from anywhere in the tree; the type safety a factory would have bought is bought instead by the runtime refusal and by the gate's literal-only rule.

The six declaring writers, each on a test a reader can check

writer declares when
sys-metadata-repository.ts the repository is env-level (organizationId == null) — #6190 option A
audit-writers.ts the audited subject resolves no organization column at all
read-audit.ts every subject in the batch does; a mixed batch declares nothing
auth-event-audit.ts always — its subject is the better-auth session object
admin-import-users.ts always — the run-level row describes sys_user
config-change-audit.ts the setting's declared scope is global

audit-writers.ts is deliberately conditional, not blanket. Case 2 of its own enumeration — the subject's organization column is present but its value is NULL — is indistinguishable at that call site from the missing-stamp defect, so declaring it would have handed the platform's largest write population back the blindness this card exists to remove.

⚠️ The reason vocabulary and the sys_audit_log ledger evidence used to contradict that, and this round fixed the texts rather than the writer. Both said the reason covered "a record whose own column is NULL"; no writer declares that case. Both are RUNTIME strings that reach operators — a reason describing a population no writer produces reads as a claim the platform makes — so both now state case 1 only, and both say explicitly that case 2 keeps meeting the refusal. ⛔ The writer was not widened to match the prose.

Zone 3 — the update path

The mirrored post-hook door does not need the declaration, and the reason is the one system-write-organization.ts already records: this resolver is INSERT-only by design, because a session write stamps the organization on insert (injectTenantOnInsert) and no write path stamps it on an update. With nothing resolving an organization on the update path there is no reader for a declaration there, and adding one would invent a second rule rather than follow the session write. The declaration also rides the options, not the row, so the update door never sees it either way.

Verification

All at f5d20ac01 (a clean tree; the gate union and the tests below were run on that commit, after the last one).

  • vitest run src/tenancy-orgless-write-declaration.test.ts src/tenancy-by-object-classification.test.ts2 files, 41 tests passed (was 39; +2 empty-batch pins). Refusals assert the ADR-0112 envelope (code + status), never a bare toThrow().
  • Reverse verification (ablation), re-run at this head. Predicted direction: RED on the acceptance pins only. Ablating if (orgLessDeclaration !== undefined) return undefined;3 failed / 38 passed: exactly the three acceptance pins, while the refusal, ledger and both new empty-batch pins stayed green (correctly — none depends on that line). Mutation confirmed on disk before the run: blob c4e4e10b…b1b4de52…, target-line occurrences 1 → 0, injected marker 0 → 1. Restore pinned to HEAD (never a bare git checkout --, which reads the index and hands the mutation back) and proven after: blob back to c4e4e10b…, marker count 0, git diff HEAD for the file empty. No dist hop — the test imports ./engine.js inside its own package, so the mutation is read from source; the dependency closure was rebuilt beforehand either way.
  • A1 repair, measured in both directions. The new empty-batch pin was written first and observed RED on the unrepaired engine (AssertionError: promise resolved "[]" instead of rejecting, 1 failed / 21 passed), then green after.
  • typecheck — objectql, spec, plugin-audit, plugin-auth, service-settings, metadata-protocol: all Done. ⚠️ objectql's tsconfig.json excludes **/*.test.ts, so that half is carried by check:test-typecheck against tsconfig.test.json — verified to actually compile the edited file (tsc --listFiles names tenancy-orgless-write-declaration.test.ts, 1 occurrence), not assumed.
  • Writers' own suites: plugin-audit 323 passed (22 files), service-settings 519 passed (29 files), metadata-protocol passed, plugin-auth admin-import-users + durability-swallow-repair 48 passed. ⚠️ Those last two first reported 3 failures that were NOT MEASURED, not red: Failed to resolve entry for package "@objectstack/rest", i.e. an unbuilt dependency. They pass once the closure is built.
  • Gate family derived with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands, no path arguments — 113 commands over 20 changed paths, identical before and after the merge, and the deriver reports the tree is no longer stale for what it derives from.
  • 38 gates run locally, all green, including the two this round turned on: check:pm-dispatch-gates (1288 cases pass, 0 failed) and check:orgless-write-declarations (+ its --self-test). Also: error-code-casing, dispatcher-error-vocabulary, nul-bytes, watch-hint-literal, declared-population-live, parse-guard, role-word, page-declaration-shape, ratchet-remedy-authority, comment-mask-adoption, comment-mask-corpus, engine-double-contract, test-source-alias, cross-package-test-inputs, changeset-gate-self-tests, durability-log-level, swallow-census-controls, pnpm-filter-targets, published-files, required-contexts, merge-driver, pm-half-states, bare-root-worklist --self-test, self-test-wired, self-test-workflow-commands, system-context-census, tenant-audit-census, closing-keyword-parity, ci-filter-parity, undeclared-dep-imports, aggregator-roster, changeset-no-major, empty-changeset, adr-0087-registration, spec check:error-code-provenance, spec check:generated (15 of 15 artifacts current on the merged tree).
  • check-system-context-census was red again after this round's engine edit — the added lines shifted the engine.ts:line anchors on the elevation-read page. Repaired with the gate's own --fix, twice (once before the merge, once on the merged tree): --fix REFUSED ZERO files on both runs, which is the gate's only signal that no elevation read site arrived or vanished — a pure re-anchor, not a population change. ⚠️ One of the 16 rewrites on the merged tree, auth-plugin.ts:1380:1405, is drift this branch did not cause; it arrived with the 15 commits merged in.
  • Lint, narrowed and the narrowing declared. eslint --no-inline-config --format json over the 14 changed source files: 14 files linted, 0 errors, 0 warnings, exit 0 — the count read from eslint's own JSON output, and 14 of 14 came back with no "file ignored" message, so none was excluded by config. The narrowing is sound because this repo's single eslint.config.mjs never enables type-aware linting for any file (no parserOptions.project, no typed @typescript-eslint rules — stated and measured with a positive control at eslint.config.mjs:327), so this diff cannot move the verdict on a file it does not contain. The repo-wide sweep is CI's run.
  • check:nul-bytes green, plus an independent control-character scan over the whole branch diff: zero hits.
  • node scripts/pm/check-governed-merges.mjs — the 20-path diff hits no governed surface (docs/adr/**, .claude/**, skills/**, AGENTS.md, CLAUDE.md); ordinary queue landing would apply, but see the unarmed notice at the top.
  • NOT MEASURED, not green: check:type-check-debt and check-test-completeness need the whole workspace built and a saved turbo run test log respectively. Neither is a finding; CI measures both.

Scope

packages/spec: one error-code ledger entry and its two generated rows, and nothing else — see the caveat above. No governed surface. No content/docs/releases/ edit. Changeset is minor — new exported symbols on published packages.

⚠️ One landed comment in system-write-organization.ts said a deliberately org-less population is declared on the OBJECT and "never a per-write bypass flag". That sentence is about a whole-object population and still stands; it is annotated rather than deleted, with the argument for why this declaration is its opposite on all three counts (it narrows, it cannot travel, and misuse throws).

🤖 Generated with Claude Code

https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68

…rring it from NULL

`resolveSystemInsertOrganization` decided per object plus posture, so an object
holding BOTH org-stamped and adjudicated org-less rows fit no verdict the
#13491 ledger could express: `tenant-scoped` refuses its own ruled-legitimate
writes on a walled install, `global` abandons the org-stamped majority. Both
specimens were parked in `unclassified`, which put the tenant-audit control's
blind spot on two of the largest write populations in the platform namespace.

The root of it is that one `NULL` meant both "deliberate" and "bug". This adds
the channel that separates them: a per-write `orgLessWrite` declaration naming
its own object and an adjudicated reason, a fourth ledger verdict
(`conditional`) that ADMITS such an object into #8844's derive-or-refuse
machinery, and a refusal for every declaration the ledger does not admit — so
the option has no silently-ignored spelling. The check runs ahead of every early
return in the resolver, which is where that property lives.

The declaration rides the write OPTIONS, not the row: the post-hook
declared-field door judges the row payload and would refuse the declaration
before the resolver it exists to inform ever saw it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
…he engine import block moved

`check:system-context-census` anchors the elevation-read page at file:line, and
the #13636 import block shifted every anchor below it in `engine.ts` (and the
read-audit flush). Repaired with the gate's own `--fix`; no prose changed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 6 package(s): @objectstack/metadata-protocol, @objectstack/objectql, @objectstack/plugin-audit, @objectstack/plugin-auth, @objectstack/service-settings, @objectstack/spec, touching 30 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/objectql/src/tenancy/system-write-organization.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

22 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 09cc6be43414214b9abfc7791b8497ea4f9dd2fc.

5 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/objectql/src/tenancy/system-write-organization.ts) — pages documenting those are invisible to this run
  • 1 anchor(s) matched too much of the corpus to be a work list: ObjectQL (symbol, 65 pages)
  • 6 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 134 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 09cc6be43414214b9abfc7791b8497ea4f9dd2fcpackageMentionDocs.

Which tree this was computed on

This run read content/docs from b0baca9119c370c033fd76f03ab8a1e1a98eed0b — the merge of head 9dce2135072fb1e3933c3fdbe5893f1ba03fe54e into base 09cc6be43414214b9abfc7791b8497ea4f9dd2fc, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin b0baca9119c370c033fd76f03ab8a1e1a98eed0b && git checkout b0baca9119c370c033fd76f03ab8a1e1a98eed0b
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 09cc6be43414214b9abfc7791b8497ea4f9dd2fc 9dce2135072fb1e3933c3fdbe5893f1ba03fe54e && git checkout -B drift-repro 09cc6be43414214b9abfc7791b8497ea4f9dd2fc && git merge --no-ff 9dce2135072fb1e3933c3fdbe5893f1ba03fe54e

node scripts/docs-audit/affected-docs.mjs --json 09cc6be43414214b9abfc7791b8497ea4f9dd2fc

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 09cc6be43414214b9abfc7791b8497ea4f9dd2fc → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…e wire code, and align the vocabulary with the writer

B1 — `check-orgless-write-declarations.mjs` spelled the repo tooling root as a
quoted literal, which the dispatch derivation reads as this gate DECLARING that
tree as a population it reads. It excludes it. Respelled as an anchored regex
(remedy (b), `check-published-files.mjs`' worked instance), so the escapable-
literal row discharges by construction rather than by a new ledger line.

B4 — `ERR_ORGLESS_WRITE_DECLARATION_REFUSED` is `status` 500 and REST forwards a
string code on any `status >= 500`, so it is wire vocabulary and belongs in the
ADR-0112 ledger beside its sibling `ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED`.
One entry plus its generated rows.

Contradiction — the reason vocabulary and the `sys_audit_log` ledger evidence
claimed a population no writer declares (a subject whose organization column is
present but NULL). `audit-writers.ts` deliberately leaves that case to the
refusal; both texts now say so, and both are runtime strings that reach
operators.

A1 — an empty batch built no row hook contexts, so a bogus declaration on one
was the single spelling of the option that was silently ignored. It now reads
the caller's options on that path, and two pins hold both directions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
…ine edit

`check-system-context-census.mjs --fix`: 9 anchors rewritten, ZERO files
refused — a pure line shift, which is the gate's only signal that no elevation
read site arrived or vanished.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
The merge's os-regen deferral, discharged as its own commit on a known-good
base (the driver merges these paths with exit 0 while silently keeping one
side, so only a regeneration on the merged tree is honest).

`--fix` rewrote 16 anchors and REFUSED ZERO files. One of them —
`auth-plugin.ts:1380` -> `:1405` — is drift this branch did not cause: it
arrived with the 15 commits merged in above.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
check:doc-authoring Rule 3b (cross-package prose-id leg) flagged the
OrgLessWriteDeclarationRefusedError message: a runtime string reaching
operators and generated surfaces cannot resolve `#13636`. Strip the id,
keep the customer-resolvable maintainer-ruling date, and move the id to
an adjacent `//` comment — the same pattern platform-object-tenancy.ts's
`evidence` strings already use for the same reason.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68
…gless-write-declaration

# Conflicts:
#	content/docs/permissions/system-context.mdx
The merge of origin/main into this branch conflicted in
content/docs/permissions/system-context.mdx (line-number anchors into
packages/objectql/src/engine.ts). Taking the origin/main side blindly
left a stale anchor: this branch's own orgless-write-declaration change
shifts engine.ts line numbers, so the correct anchor is the pre-merge
(this branch's) value, confirmed against check:system-context-census's
diagnostic (ledger-excused site 10069, read site 10086).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0112hMx9hjJ9BgB28X97DS68

os-musk commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator Author

Merging paused on maintainer instruction — handed over, not abandoned

Provenance. Instructed by the maintainer (elon@objectstack.ai) in the domain:engine execution PM session session_0112hMx9hjJ9BgB28X97DS68, 2026-09-03. Verbatim, untranslated:

14923 暂停合并
交维护者处理

This PR stays draft — which is the actual mechanism behind the pause, since a draft PR can neither be merged nor have auto-merge armed. It was never armed; the ⛔ UNARMED notice the implementing seat put at the top of this body stands.

Card #13636 moved pm:dispatchedpm:awaiting-maintainer, assignee cleared, needs:contract-review retained on both the card and this PR. Full handover record, including the state table and the read-back verification, is on the card: comment 5529193579.

Three facts about what you are picking up, so none of them is a surprise:

  1. ⚠️ There is a merge conflict now. mergeable_state: dirty — this branch's base is recorded at 09cc6be43 and main has since moved to 5bc2f2727. Deliberately not resolved: resolving it is driving the PR, and the instruction stops that.
  2. A sys_audit_log write refused by the system-write organization rule is swallowed by plugin-audit's best-effort catch, so the audit row about a defective record is LOST silently — surfaced by #13636's admission #14927 is still open — the plugin-audit best-effort catch that swallows a refused sys_audit_log write, including the audit row for the env-level sys_metadata write this PR declares. priority:p1, security, domain:services, pm:queue. The implementing seat surfaced it rather than narrowing this PR's admission to dodge it, per the ruling's constraint 3.
  3. The clause-② contract review never ran. It requires the CONTRACT_REVIEW_TIER model, which this seat does not have and which was quota-exhausted through every attempt today. So needs:contract-review is unsatisfied because no review happened — not because one failed. Those are different states, and the label cannot distinguish them, so it is written here.

⛔ No further work will be pushed to this branch by this seat.


Generated by Claude Code

os-zhuang pushed a commit that referenced this pull request Sep 3, 2026
…hen an organization authored them, no platform organization

Rewritten on the maintainer's 2026-09-04 rulings (verbatim in the record): declared metadata is
never seeded; Studio/build-agent metadata is DB-resident and UI-editable with no organization
column; organization-level template editing stays closed (copy-on-write if ever opened);
references by name, registry-first; deployment-level state has no column; #13636 / PR #14923
superseded (§1.6); the platform organization of the first draft is recorded as a rejected
alternative.

Refs #13564 #13636

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

os-musk commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator Author

Voided on maintainer instruction — closing without merging

Provenance. Instructed by the maintainer (elon@objectstack.ai) in the domain:engine execution PM session session_0112hMx9hjJ9BgB28X97DS68, 2026-09-03, immediately following the pause instruction recorded in comment 5529196473. Verbatim, untranslated:

14923 直接作废

Closing this PR unmerged on that instruction. This is a maintainer disposition, not an error and not an abandonment by the seat — recorded explicitly so a sibling seat does not read the closure as a mis-sweep and reopen it.

Scope of the voiding, stated precisely

The instruction names 14923, this pull request. It does not name card #13636, and this seat does not extend it: #13636 stays open at pm:awaiting-maintainer with its ruling history intact, and its disposition is the maintainer's remaining action. needs:contract-review stays on the card — ⛔ a gate is cleared only at a PASS, never because the work behind it was discarded.

What is discarded with it

Head 9dce21350, 20 files, +1405 / −34, unmerged. The branch claude/issue-13636-orgless-write-declaration is left in place — nothing is deleted, so the work is recoverable if any of it is wanted later. The substantive pieces going away with this PR:

  • The conditional tenancy verdict and the orgLessWrite: { object, reason } write-option declaration, with its closed reason vocabulary and its refusal (ERR_ORGLESS_WRITE_DECLARATION_REFUSED).
  • The new gate pnpm check:orgless-write-declarations, and its repair of the bare-root watch-hint literal.
  • One packages/spec error-code-ledger entry and the two generated rows it produced.

One correction this PR established that outlives it

Its census re-derivation settled that the ledger header at packages/objectql/src/tenancy/platform-object-tenancy.ts:32-3784 / 25 / 59 — is correct and has not drifted. An earlier claim that it had drifted to 28 / 56 was wrong: it came from treating managedBy: 'better-auth' as "no tenant field resolves", which resolveTenantFieldName never does — sys_member, sys_team and sys_invitation are better-auth managed and declare organization_id, so all three are in the machinery's reach. That reading is independent of the direction this PR implemented, and is recorded here so it does not have to be re-derived by whoever looks next.

⛔ Nothing further will be pushed to this branch.


Generated by Claude Code

@os-musk os-musk closed this Sep 3, 2026
@hotlong

hotlong commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Closed unmerged on the maintainer's ruling (2026-09-04, live chat: 「13636 的 pr 应该作废是吧」→ 「接受你的建议」).

This PR makes a legitimately org-less write a declared state. ADR-0131 (PR #14976, §1.6) resolves the same root the other way: a row that is legitimately org-less is a row on an object that should carry no organization columnsys_metadata becomes the tenant-less environment ledger, sys_audit_log a deployment ledger whose organization attribution is a plain data field. Once the column is gone there is nothing to declare, and a declared-NULL mechanism would keep alive exactly the state ADR-0131 D1 removes.

#13636 stays open until ADR-0131 merges and then closes as superseded (ADR-0131 §8, C11). Nothing here is lost: the two specimens it measured (sys_metadata, sys_audit_log) are the first two entries of ADR-0131 D7.

🤖 Generated with Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation needs:contract-review size/xl tests tooling

Projects

None yet

3 participants